#!/usr/bin/env python
# $Id: rfc4193.py 739bcfc39881 2012/12/02 17:10:07 pgurumur $
# Copyright (c) 2009 Prabhu Gurumurthy  <pgurumur@gmail.com>
# 
# Permission to use, copy, modify, and distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
#
__version__  = "$Revision: 739bcfc39881 $"
__author__   = "$Author: pgurumur $"
__modified__ = "$Date: 2012-12-02 17:10:07Z $"

# Python imports
import ConfigParser
import os
import optparse
import sys

# Local imports
from venya.lib import cmdparser
from venya.lib import exception
from venya.lib import ip
from venya.lib import ipv6
from venya.lib import stdlib
from venya.lib import sql

__version__ = "$Revision: 739bcfc39881 $"
__author__ = "$Author: pgurumur $"

class rfc4193(cmdparser.GetOpt):
   """
   Subclass of GetOpt class from cmdparser
   This class generates IPv6 network or part of the IPv6 address (depending on
   the length requested (-l switch) loosely based on RFC 4193.

   Odd number for either address or prefix is not supported yet, more work
   will be done on later versions
   """

   ###
   """
   Option list meta data, expands the default meta data from GetOpt class
   """
   option_list = (
         optparse.make_option("-a", "--address", type = "str",
            help = "Generate a child prefix for this address",
            dest = "address"),
         optparse.make_option("-c", "--config", type = "str",
            help = "DB Config file, default: db.cfg",
            default = "db.cfg", dest = "config"),
         optparse.make_option("-i", "--insert", type = "str",
            help = "Insert the comment for the prefix in the DB",
            dest = "insert"),
         optparse.make_option("-n", "--nan", action = "store_true",
            help = "Specify to indicate a NAN prefix", default = False,
            dest = "nan"),
         optparse.make_option("-l", "--length", type = "int",
            help = "Length of the IPv6 address/prefix min: 12, max: 128",
            dest = "length"),
         optparse.make_option("-L", "--list", action = "store_true",
            help = "List all the IPv6 addresses in the database(s)",
            dest = "v6list"),
         optparse.make_option("-p", "--prefix", action = "store_true",
            help = "Generate prefix, not just the IPv6 node address",
            dest = "prefix"),
         optparse.make_option("-w", "--wan", action = "store_true",
            help = "Specify to indicate a WAN prefix", default = False,
            dest = "wan"),
   ) + cmdparser.GetOpt.option_list

   help = """
   RFC 4193 based IPv6 address generation tool using Local time and
   randomization. This tool can get either a subnet or a node address or a
   child address to a parent prefix. At this moment only even number of values 
   that are divisible by 4 are supported for prefix.

   This tool expects the system that it is running on has reasonably accurate
   NTP time sync.

   Connection to netdot and ssn database is required. SQLAlchemy is used for
   SQL connection.

   This tool by default searches netdot and IPv6_Prefixes tables and verifies
   the uniqueness of the generated prefix within thoses databases.

   To generate a unique prefix use -p and specify the length of the prefix using
   -l option.

   To generate a unique prefix within an address use -a/--address option and
   specify the address and specify -p and -l with length of the prefix.

   -i/--insert option is mandatory for inserting the prefix into any database.

   Specify -w/--wan and/or -n/--nan option along with -i/--insert to insert
   the prefix as either WAN or NAN type into the database, these are mutually
   exclusive options specifying both of them will result in error being thrown
   by the program. -w/--wan and -n/--nan are not mandatory options to insert
   into netdot, they are mandatory to insert into ssn IPv6 Prefixes table

   Use -v if you want more verbose data.

   -l/--length option value MUST BE DIVISIBLE BY 4

   Examples:
   1. Generate a Node address (The output will never get inserted into the DB)
   rfc4193.py -l 64

   2. Generate a unique prefix of length 48 (Does not inserted into the DB)
   rfc4193.py -p -l 48

   3. Generate a unique prefix of length 48 and insert into database
   rfc4193.py -p -l 48 -i "My Prefix"

   4. Generate a unique WAN prefix of length 48 and insert into database
   rfc4193.py -p -l 48 -i "My_Prefix" -w
   Note: No need to specify WAN with the insert switch/option

   5. Generate a unique NAN prefix of length 48 and insert into database
   rfc4193.py -p -l 48 -i "My_Prefix" -n
   Note: No need to specify NAN with the insert switch/option

   6. Generate a unique prefix of length 48 within a parent subnet
   rfc4193.py -p -l 48 -a fdc9:ccbc::/32
   Note: parent subnet must exist in netdot, otherwise the script will error out

   7. Generate a unique prefix of length 48 within a parent subnet and insert
      into database
   rfc4193.py -p -l 48 -a fdc9:ccbc::/32 -i "My Child Prefix"
   Note: parent subnet must exist in netdot, otherwise the script will error out

   8. Generate a unique WAN prefix of length 48 within a parent subnet and
      insert into databse
   rfc4193.py -p -l 48 -a fdc9:ccbc::/32 -i "My_Child_Prefix" -w
   Note:
      a. parent subnet must exist in netdot, otherwise the script will error out
      b. No need to specify WAN with the insert switch/option

   9. Generate a unique NAN prefix of length 48 within a parent subnet and
      insert into databse
   rfc4193.py -p -l 48 -a fdc9:ccbc::/32 -i "My_Child_Prefix" -n
   Note:
      a. parent subnet must exist in netdot, otherwise the script will error out
      b. No need to specify NAN with the insert switch/option

   For any of the above 9 steps -v/--verbosity option can be specified with
   value 1 or 2 and it will more information as needed

   For any of the above 9 steps longer option may be substituted
   """

   def __init__(self, **Keywords):
      """
      @brief init function that just calls super to instantiate the object
      @param self 'this' pointer for the class
      @param Keywords functional parameter for the class
      @return returns the object (subclass of GetOpt class)
      """
      super(rfc4193, self).__init__(**Keywords)
      self._confdb = None
      self._ssndb = None
      self._netdb = None
      self._ipblock = None
      self._ipprefix = None
      self._address = None
      self._verbose = 0
      self._rfcaddr = None

   def printf(self, fmt, *args):
      """
      @brief wrapper for print function using format and arguments
      @param self 'this' pointer for the class
      @param fmt format string
      @param args arguments corresponding for format
      """
      print("%s: %s" %(str(self), stdlib.sprintf(fmt, *args)))

   def conflict(self, address, parent = None):
      retval = None
      for ipaddr in self._address:
         if parent:
            if len(address) <= len(ipaddr):
               if str(address) == str(ipaddr):
                  retval = ipaddr
                  break
               elif ipaddr in address:
                  retval = ipaddr
                  break
         else:
            if address in ipaddr or ipaddr in address:
               retval = ipaddr
               break

      if retval is None:
         for ipaddr in self._rfcaddr:
            if parent:
               if len(address) <= len(ipaddr):
                  if str(address) == str(ipaddr):
                     retval = ipaddr
                     break
                  elif ipaddr in address:
                     retval = ipaddr
                     break
            else:
               if address in ipaddr or ipaddr in address:
                  retval = ipaddr
                  break

      return retval

   def GetChildPrefix(self, address, length):
      """
      @brief Get child prefix for a given parent subnet
      @param address parent subnet, expects a valid v6 prefix
      @param length length of the subnet needed in multiples of 4 between 4 & 12
      @return a valid non conflicting prefix
      """
      retval = None
      if length <= 128:
         flag = True
         while flag:
            temp = self.GetIPv6(128 - len(address), False)
            addr = ip.ip(int(address) + int("".join(temp.split(":")), 16),
                  version = 6, prefix = length)
            retval = ip.ip("%s/%d" %(addr[0], length))
            conflict = self.conflict(retval, address)
            if not conflict:
               flag = False
            else:
               if self._verbose:
                  self.printf("parent: %s: %s conflict with %s", address,
                        conflict, retval)

      return retval

   def handle(self):
      """
      @brief parse the command line and call GetIPv6 function
      @param self 'this' pointer for the class
      @return None, void function
      """

      # Parse the command lin
      (options, args) = self.parse()
      address = None
      prefix = False
      time = False
      length = 0 
      v6prefix = None

      if options.verbosity:
         # Verify whether verbose flag is defined, does not matter whether it
         # is 1 or 2 as long as it is defined verbose flag is et
         self._verbose = bool(options.verbosity)

      # Verify whether WAN and NAN option are both defined at the same time
      # If they are error is thrown
      if options.wan and options.nan:
         self.error("Cannot specify both WAN and NAN options at the same time")

      # Use built in parser similar to ini config
      if options.config and os.path.exists(options.config):
         parser = ConfigParser.SafeConfigParser()
         parser.read(options.config)

         try:
            # If prefix is defined then get netdot and ssn handle
            if options.prefix:
               self._netdb = sql.mysql(parser.get("Netdot", "dbname"),
                     parser.get("Netdot", "dbhost"),
                     parser.get("Netdot", "dbuser"),
                     parser.get("Netdot", "dbpass"))
               self._ssndb = sql.mysql(parser.get("RFC2893", "dbname"),
                     parser.get("RFC2893", "dbhost"),
                     parser.get("RFC2893", "dbuser"),
                     parser.get("RFC2893", "dbpass"))
         except exception.SQLError as err:
            self.error(err.message)
         else:
            # If prefix is defined, get ip tables from netdot and ssn
            # database
            if options.prefix:
               try:
                  self._ipprefix = self._ssndb.IPv6_Prefix
                  self._ipblock = self._netdb.ipblock
                  # Get the list of IPv6 addresses from ipblock table in netdot
                  self._address = [ ip.ip(
                     int(addr), version = 6, prefix = prefix) for (
                        addr, prefix) in self._ipblock.q.filter(
                           self._ipblock.version == 6).order_by(
                              self._ipblock.address).values(
                                 self._ipblock.address, self._ipblock.prefix)]

                  # Get the list of IPv6 addresses from IPv6 prefixes table in 
                  # ssn
                  self._rfcaddr = [ip.ip(
                     prefix[0]) for prefix in self._ipprefix.q.order_by(
                        self._ipprefix.prefix).values(self._ipprefix.prefix)]
               except exception.SQLError as err:
                  self.error(err.args[0])

      else:
         self.error("either config file not defined or not found!")

      if options.v6list:
         print("Netdot: ")
         for ix in sorted(self._address):
            print("\t%s" %ix)

         print("RFC DB: ")
         for ix in sorted(self._rfcaddr):
            print("\t%s" %ix)
      else:
         if options.length:
            try:
               length = int(options.length)
            except ValueError, message:
               self.error(message)
         else:
            self.error("no length defined!")

         if options.prefix:
            if options.address:
               try:
                  address = ip.ip(options.address)
               except exception.IPError as err:
                  self.error(err.message)
            
               if address.version != 6:
                  self.error("IP address needs to be version 6")

               if self.GetParentId(address) == 0:
                  self.error("parent does not exist in netdot, invalid request")

               if len(address) < length and self.GetParentId(address):
                  v6prefix = self.GetChildPrefix(address, length)
                  self.printf("%s child prefix of length %d: %s", str(address),
                        length, v6prefix)
               else:
                  self.error("child prefix length should be longer than parent")
            else:
               v6prefix = self.GetIPv6(length, True)
               self.printf("prefix of %d bit length: %s", length, v6prefix)

            if options.insert:
               self.dbinsert(v6prefix, options.insert, address, options.wan,
                     options.nan)
         else:
            retval = self.GetIPv6(length, False)
            self.printf("node of %s bit length: %s", length,
                  retval)

   def GetParentId(self, address):
      retval = 0
      if address:
         addrid = [addrid[0] for addrid in self._ipblock.q.filter(
            self._ipblock.address == int(address)).values(
               self._ipblock.id)]
         retval = addrid[0]

      return retval

   def dbinsert(self, prefix, descr, address = None, wan = False, nan = False):
      addrid = 0
      description = None
      if wan:
         description = "%s Wan" %str(descr)
      elif nan:
         description = "%s Nan" %str(descr)

      if address:
         addrid = self.GetParentId(address)

      retval = None
      if description:
         retval = self._ipblock.SQLInsert(address = int(prefix),
               prefix = len(prefix), parent = addrid, owner = 25, status = 0,
               description = description, used_by = 25, interface = 0, vlan = 0,
               version = prefix.version)
         self.printf("Inserted %s on table %s, inserted id: %d",
               prefix, str(self._ipblock), retval)
         retval = self._ipprefix.SQLInsert(purpose = description,
               prefix = str(prefix))
         self.printf("Inserted %s on table %s, inserted id: %d",
               prefix, str(self._ipprefix), retval)
         """
         self._ippinsrt.execute(prefix = str(prefix), purpose = description)
         """
      else:
         retval = self._ipblock.SQLInsert(address = int(prefix),
               prefix = len(prefix), parent = addrid, description = descr,
               owner = 25, used_by = 25, version = prefix.version, vlan = 0,
               interface = 0, status = 0)
         self.printf("Inserted %s on table %s, inserted id: %d",
               prefix, str(self._ipblock), retval)
         
   def GetIPv6(self, Length, Prefix, Time = False):
      """
      @brief Actual function that instanties the ipv6 class and get the ipv6
             address or network
      @param self 'this' pointer for the class
      @param Length length of the address or network
      @param prefix Boolean value to indicate that prefix (network) address
      @param Time Use local time instead of NTP time to stir the randomness
      @return IPv6 address or network
      """
      address = None
      # Maximum and minimum value of the IPv6 network/address that can be
      # generated
      ipv6MaxLen = 128
      ipv6MinLen = 12

      # Few sanity checks, all are self explantory, throws assertion error
      if (Length <= 0):
         self.error("length cannot be zero or less than zero")
      elif (Length > ipv6MaxLen):
         self.error("length cannot be greater than %d" %ipv6MaxLen)

      if (Prefix and (Length < ipv6MinLen)):
         self.error("length is too short to generate prefix based on RFC 4193")

      if (Length < ipv6MinLen):
         self.error("length too short to generate IPv6 node address")

      # Instantiate the IPv6 class with the length and prefix variables
      try:
         addr = ipv6.rfc4193(Length, Prefix)
      except exception.RFCError as err:
         self.error(err.message)

      if Prefix:
         flag = True
         while flag:
            try:
               try:
                  address = addr.GetIPv6()
               except exception.RFCError as err:
                  self.error(err)

               conflict = self.conflict(addr.ip)
               if not conflict:
                  flag = False
               else:
                  self.printf("%s conflicts with %s\n", addr.ip, conflict)
            except KeyboardInterrupt:
               self.error("interrupted by keyboard!")
      else:
         try:
            address = addr.GetIPv6()
         except exception.RFCError as err:
            self.error(err)
         
      # return the IPv6 address
      return address

# If the namespace is main
if __name__ == "__main__":
   rfcopt = None
   try:
      # Construct the RFC 4193 tool command line options
      rfcopt = rfc4193(version = 1.6, args = sys.argv)
      # If we have few parameter, then print help and exit
      if len(rfcopt) <= 1:
         rfcopt.printhelp()
      else:
         # If we have correct number of arguments, call the default handler
         # (handle()) function, any errors will be GetOptError,
         # print the message
         # and exits with code 1
         rfcopt.handle()
   except exception.GetOptError as err:
      print(err)
      sys.exit(1)
